1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  package org.apache.tapestry5.internal.services;
14  
15  import org.apache.tapestry5.internal.parser.*;
16  import org.apache.tapestry5.ioc.Location;
17  import org.apache.tapestry5.ioc.Resource;
18  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
19  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
20  import org.apache.tapestry5.ioc.internal.util.TapestryException;
21  import org.apache.tapestry5.ioc.util.ExceptionUtils;
22  
23  import javax.xml.namespace.QName;
24  import java.net.URL;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  import static org.apache.tapestry5.internal.services.SaxTemplateParser.Version.*;
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  @SuppressWarnings(
46          {"JavaDoc"})
47  public class SaxTemplateParser
48  {
49      private static final String MIXINS_ATTRIBUTE_NAME = "mixins";
50  
51      private static final String TYPE_ATTRIBUTE_NAME = "type";
52  
53      private static final String ID_ATTRIBUTE_NAME = "id";
54  
55      public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
56  
57      private static final Map<String, Version> NAMESPACE_URI_TO_VERSION = CollectionFactory.newMap();
58  
59      {
60          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_0_0.xsd", T_5_0);
61          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_1_0.xsd", T_5_1);
62          
63          
64          
65          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_3.xsd", T_5_3);
66          
67          
68          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_4.xsd", T_5_4);
69      }
70  
71      
72  
73  
74  
75  
76      private static final String TAPESTRY_PARAMETERS_URI = "tapestry:parameter";
77  
78      
79  
80  
81  
82      private static final String LIB_NAMESPACE_URI_PREFIX = "tapestry-library:";
83  
84      
85  
86  
87  
88  
89  
90      private static final Pattern LIBRARY_PATH_PATTERN = Pattern.compile("^[a-z]\\w*(/[a-z]\\w*)*$",
91              Pattern.CASE_INSENSITIVE);
92  
93      private static final Pattern ID_PATTERN = Pattern.compile("^[a-z]\\w*$",
94              Pattern.CASE_INSENSITIVE);
95  
96      
97  
98  
99  
100 
101 
102     private static final Pattern REDUCE_LINEBREAKS_PATTERN = Pattern.compile(
103             "[ \\t\\f]*[\\r\\n]\\s*", Pattern.MULTILINE);
104 
105     
106 
107 
108 
109 
110     private static final Pattern REDUCE_WHITESPACE_PATTERN = Pattern.compile("[ \\t\\f]+",
111             Pattern.MULTILINE);
112 
113     
114     
115     
116     
117 
118     private static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{\\s*(((?!\\$\\{).)*)\\s*}");
119     private static final char EXPANSION_STRING_DELIMITTER = '\'';
120     private static final char OPEN_BRACE = '{';
121     private static final char CLOSE_BRACE = '}';
122 
123     private static final Set<String> MUST_BE_ROOT = CollectionFactory.newSet("extend", "container");
124 
125     private final Resource resource;
126 
127     private final XMLTokenStream tokenStream;
128 
129     private final StringBuilder textBuffer = new StringBuilder();
130 
131     private final List<TemplateToken> tokens = CollectionFactory.newList();
132 
133     
134     
135     private List<TemplateToken> tokenAccumulator = tokens;
136 
137     
138 
139 
140 
141     private final Map<String, Location> componentIds = CollectionFactory.newCaseInsensitiveMap();
142 
143     
144 
145 
146 
147 
148     private Map<String, List<TemplateToken>> overrides;
149 
150     private boolean extension;
151 
152     private Location textStartLocation;
153 
154     private boolean active = true;
155 
156     private boolean strictMixinParameters = false;
157 
158     private final Map<String, Boolean> extensionPointIdSet = CollectionFactory.newCaseInsensitiveMap();
159 
160     public SaxTemplateParser(Resource resource, Map<String, URL> publicIdToURL)
161     {
162         this.resource = resource;
163         this.tokenStream = new XMLTokenStream(resource, publicIdToURL);
164     }
165 
166     public ComponentTemplate parse(boolean compressWhitespace)
167     {
168         try
169         {
170             tokenStream.parse();
171 
172             TemplateParserState initialParserState = new TemplateParserState()
173                     .compressWhitespace(compressWhitespace);
174 
175             root(initialParserState);
176 
177             return new ComponentTemplateImpl(resource, tokens, componentIds, extension, strictMixinParameters, overrides);
178         } catch (Exception ex)
179         {
180             throw new TapestryException(String.format("Failure parsing template %s: %s", resource,
181                     ExceptionUtils.toMessage(ex)), tokenStream.getLocation(), ex);
182         }
183 
184     }
185 
186     void root(TemplateParserState state)
187     {
188         while (active && tokenStream.hasNext())
189         {
190             switch (tokenStream.next())
191             {
192                 case DTD:
193 
194                     dtd();
195 
196                     break;
197 
198                 case START_ELEMENT:
199 
200                     rootElement(state);
201 
202                     break;
203 
204                 case END_DOCUMENT:
205                     
206                     break;
207 
208                 default:
209                     textContent(state);
210             }
211         }
212     }
213 
214     private void rootElement(TemplateParserState initialState)
215     {
216         TemplateParserState state = setupForElement(initialState);
217 
218         String uri = tokenStream.getNamespaceURI();
219         String name = tokenStream.getLocalName();
220         Version version = NAMESPACE_URI_TO_VERSION.get(uri);
221 
222         if (T_5_1.sameOrEarlier(version))
223         {
224             if (name.equalsIgnoreCase("extend"))
225             {
226                 extend(state);
227                 return;
228             }
229         }
230 
231         if (version != null)
232         {
233             if (name.equalsIgnoreCase("container"))
234             {
235                 container(state);
236                 return;
237             }
238         }
239 
240         element(state);
241     }
242 
243     private void extend(TemplateParserState state)
244     {
245         extension = true;
246 
247         while (active)
248         {
249             switch (tokenStream.next())
250             {
251                 case START_ELEMENT:
252 
253                     if (isTemplateVersion(Version.T_5_1) && isElementName("replace"))
254                     {
255                         replace(state);
256                         break;
257                     }
258 
259                     boolean is54 = isTemplateVersion(Version.T_5_4);
260 
261                     if (is54 && isElementName("block"))
262                     {
263                         block(state);
264                         break;
265                     }
266 
267                     throw new RuntimeException(
268                             is54
269                                     ? "Child element of <extend> must be <replace> or <block>."
270                                     : "Child element of <extend> must be <replace>.");
271 
272                 case END_ELEMENT:
273 
274                     return;
275 
276                 
277 
278                 case COMMENT:
279                 case SPACE:
280                     break;
281 
282                 
283 
284                 case CHARACTERS:
285                     if (InternalUtils.isBlank(tokenStream.getText()))
286                         break;
287 
288                 default:
289                     unexpectedEventType();
290             }
291         }
292     }
293 
294     
295 
296 
297     private boolean isElementName(String elementName)
298     {
299         return tokenStream.getLocalName().equalsIgnoreCase(elementName);
300     }
301 
302     
303 
304 
305     private boolean isTemplateVersion(Version requiredVersion)
306     {
307         Version templateVersion = NAMESPACE_URI_TO_VERSION.get(tokenStream.getNamespaceURI());
308 
309         return requiredVersion.sameOrEarlier(templateVersion);
310     }
311 
312     private void replace(TemplateParserState state)
313     {
314         String id = getRequiredIdAttribute();
315 
316         addContentToOverride(setupForElement(state), id);
317     }
318 
319     private void unexpectedEventType()
320     {
321         XMLTokenType eventType = tokenStream.getEventType();
322 
323         throw new IllegalStateException(String.format("Unexpected XML parse event %s.", eventType
324                 .name()));
325     }
326 
327     private void dtd()
328     {
329         DTDData dtdInfo = tokenStream.getDTDInfo();
330 
331         tokenAccumulator.add(new DTDToken(dtdInfo.rootName, dtdInfo.publicId, dtdInfo
332                 .systemId, getLocation()));
333     }
334 
335     private Location getLocation()
336     {
337         return tokenStream.getLocation();
338     }
339 
340     
341 
342 
343 
344 
345 
346 
347 
348 
349 
350 
351 
352 
353 
354 
355 
356 
357 
358 
359 
360 
361 
362 
363 
364 
365     void element(TemplateParserState initialState)
366     {
367         TemplateParserState state = setupForElement(initialState);
368 
369         String uri = tokenStream.getNamespaceURI();
370         String name = tokenStream.getLocalName();
371         Version version = NAMESPACE_URI_TO_VERSION.get(uri);
372 
373         if (T_5_1.sameOrEarlier(version))
374         {
375 
376             if (name.equalsIgnoreCase("remove"))
377             {
378                 removeContent();
379 
380                 return;
381             }
382 
383             if (name.equalsIgnoreCase("content"))
384             {
385                 limitContent(state);
386 
387                 return;
388             }
389 
390             if (name.equalsIgnoreCase("extension-point"))
391             {
392                 extensionPoint(state);
393 
394                 return;
395             }
396 
397             if (name.equalsIgnoreCase("replace"))
398             {
399                 throw new RuntimeException(
400                         "The <replace> element may only appear directly within an extend element.");
401             }
402 
403             if (MUST_BE_ROOT.contains(name))
404                 mustBeRoot(name);
405         }
406 
407         if (version != null)
408         {
409 
410             if (name.equalsIgnoreCase("body"))
411             {
412                 body();
413                 return;
414             }
415 
416             if (name.equalsIgnoreCase("container"))
417             {
418                 mustBeRoot(name);
419             }
420 
421             if (name.equalsIgnoreCase("block"))
422             {
423                 block(state);
424                 return;
425             }
426 
427             if (name.equalsIgnoreCase("parameter"))
428             {
429                 if (T_5_3.sameOrEarlier(version))
430                 {
431                     throw new RuntimeException(
432                             String.format("The <parameter> element has been deprecated in Tapestry 5.3 in favour of '%s' namespace.", TAPESTRY_PARAMETERS_URI));
433                 }
434 
435                 classicParameter(state);
436 
437                 return;
438             }
439 
440             possibleTapestryComponent(state, null, tokenStream.getLocalName().replace('.', '/'));
441 
442             return;
443         }
444 
445         if (uri != null && uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
446         {
447             libraryNamespaceComponent(state);
448 
449             return;
450         }
451 
452         if (TAPESTRY_PARAMETERS_URI.equals(uri))
453         {
454             parameterElement(state);
455 
456             return;
457         }
458 
459         
460 
461         possibleTapestryComponent(state, tokenStream.getLocalName(), null);
462     }
463 
464     
465 
466 
467 
468 
469 
470 
471 
472     private void processBody(TemplateParserState state)
473     {
474         while (active)
475         {
476             switch (tokenStream.next())
477             {
478                 case START_ELEMENT:
479 
480                     
481 
482                     element(state);
483                     break;
484 
485                 case END_ELEMENT:
486 
487                     
488                     
489                     
490 
491                     endElement(state);
492 
493                     return;
494 
495                 default:
496                     textContent(state);
497             }
498         }
499     }
500 
501     private TemplateParserState setupForElement(TemplateParserState initialState)
502     {
503         processTextBuffer(initialState);
504 
505         return checkForXMLSpaceAttribute(initialState);
506     }
507 
508     
509 
510 
511 
512 
513 
514     private void extensionPoint(TemplateParserState state)
515     {
516         
517         
518         
519         
520 
521         String id = getRequiredIdAttribute();
522 
523         if (extensionPointIdSet.containsKey(id))
524         {
525             throw new TapestryException(String.format("Extension point '%s' is already defined for this template. Extension point ids must be unique.", id), getLocation(), null);
526         } else
527         {
528             extensionPointIdSet.put(id, true);
529         }
530 
531         tokenAccumulator.add(new ExtensionPointToken(id, getLocation()));
532 
533         addContentToOverride(state.insideComponent(false), id);
534     }
535 
536     private String getRequiredIdAttribute()
537     {
538         String id = getSingleParameter("id");
539 
540         if (InternalUtils.isBlank(id))
541             throw new RuntimeException(String.format("The <%s> element must have an id attribute.",
542                     tokenStream.getLocalName()));
543 
544         return id;
545     }
546 
547     private void addContentToOverride(TemplateParserState state, String id)
548 
549     {
550         List<TemplateToken> savedTokenAccumulator = tokenAccumulator;
551 
552         tokenAccumulator = CollectionFactory.newList();
553 
554         
555         
556         
557 
558         if (overrides == null)
559             overrides = CollectionFactory.newCaseInsensitiveMap();
560 
561         overrides.put(id, tokenAccumulator);
562 
563         while (active)
564         {
565             switch (tokenStream.next())
566             {
567                 case START_ELEMENT:
568                     element(state);
569                     break;
570 
571                 case END_ELEMENT:
572 
573                     processTextBuffer(state);
574 
575                     
576                     
577 
578                     tokenAccumulator = savedTokenAccumulator;
579                     return;
580 
581                 default:
582                     textContent(state);
583             }
584         }
585     }
586 
587     private void mustBeRoot(String name)
588     {
589         throw new RuntimeException(String.format(
590                 "Element <%s> is only valid as the root element of a template.", name));
591     }
592 
593     
594 
595 
596 
597 
598     private void limitContent(TemplateParserState state)
599     {
600         if (state.isCollectingContent())
601             throw new IllegalStateException(
602                     "The <content> element may not be nested within another <content> element.");
603 
604         TemplateParserState newState = state.collectingContent().insideComponent(false);
605 
606         
607 
608         tokens.clear();
609 
610         
611         
612         
613         
614 
615         overrides = null;
616 
617         
618         
619         
620 
621         tokenAccumulator = tokens;
622 
623         while (active)
624         {
625             switch (tokenStream.next())
626             {
627                 case START_ELEMENT:
628                     element(newState);
629                     break;
630 
631                 case END_ELEMENT:
632 
633                     
634                     
635                     
636 
637                     processTextBuffer(newState);
638 
639                     active = false;
640 
641                     break;
642 
643                 default:
644                     textContent(state);
645             }
646         }
647 
648     }
649 
650     private void removeContent()
651     {
652         int depth = 1;
653 
654         while (active)
655         {
656             switch (tokenStream.next())
657             {
658                 case START_ELEMENT:
659                     depth++;
660                     break;
661 
662                 
663 
664                 case END_ELEMENT:
665                     depth--;
666 
667                     if (depth == 0)
668                         return;
669 
670                     break;
671 
672                 default:
673                     
674             }
675         }
676     }
677 
678     private String nullForBlank(String input)
679     {
680         return InternalUtils.isBlank(input) ? null : input;
681     }
682 
683     
684 
685 
686     private void libraryNamespaceComponent(TemplateParserState state)
687     {
688         String uri = tokenStream.getNamespaceURI();
689 
690         
691 
692         String path = uri.substring(LIB_NAMESPACE_URI_PREFIX.length());
693 
694         if (!LIBRARY_PATH_PATTERN.matcher(path).matches())
695             throw new RuntimeException(String.format("The path portion of library namespace URI '%s' is not valid: it must be a simple identifier, or a series of identifiers seperated by slashes.", uri));
696 
697         possibleTapestryComponent(state, null, path + "/" + tokenStream.getLocalName());
698     }
699 
700     
701 
702 
703 
704 
705 
706     private void possibleTapestryComponent(TemplateParserState state, String elementName,
707                                            String identifiedType)
708     {
709         String id = null;
710         String type = identifiedType;
711         String mixins = null;
712 
713         int count = tokenStream.getAttributeCount();
714 
715         Location location = getLocation();
716 
717         List<TemplateToken> attributeTokens = CollectionFactory.newList();
718 
719         for (int i = 0; i < count; i++)
720         {
721             QName qname = tokenStream.getAttributeName(i);
722 
723             if (isXMLSpaceAttribute(qname))
724                 continue;
725 
726             
727 
728             String localName = qname.getLocalPart();
729 
730             if (InternalUtils.isBlank(localName))
731                 continue;
732 
733             String uri = qname.getNamespaceURI();
734 
735             String value = tokenStream.getAttributeValue(i);
736 
737 
738             Version version = NAMESPACE_URI_TO_VERSION.get(uri);
739 
740             if (version != null)
741             {
742                 
743                 
744 
745                 if (T_5_4.sameOrEarlier(version)) {
746                     strictMixinParameters = true;
747                 }
748 
749                 if (localName.equalsIgnoreCase(ID_ATTRIBUTE_NAME))
750                 {
751                     id = nullForBlank(value);
752 
753                     validateId(id, "Component id '%s' is not valid; component ids must be valid Java identifiers: start with a letter, and consist of letters, numbers and underscores.");
754 
755                     continue;
756                 }
757 
758                 if (type == null && localName.equalsIgnoreCase(TYPE_ATTRIBUTE_NAME))
759                 {
760                     type = nullForBlank(value);
761                     continue;
762                 }
763 
764                 if (localName.equalsIgnoreCase(MIXINS_ATTRIBUTE_NAME))
765                 {
766                     mixins = nullForBlank(value);
767                     continue;
768                 }
769 
770                 
771                 
772                 
773                 
774             }
775 
776             attributeTokens.add(new AttributeToken(uri, localName, value, location));
777         }
778 
779         boolean isComponent = (id != null || type != null);
780 
781         
782         
783 
784         if (mixins != null && !isComponent)
785             throw new TapestryException(String.format("You may not specify mixins for element <%s> because it does not represent a component (which requires either an id attribute or a type attribute).", elementName),
786                     location, null);
787 
788         if (isComponent)
789         {
790             tokenAccumulator.add(new StartComponentToken(elementName, id, type, mixins, location));
791         } else
792         {
793             tokenAccumulator.add(new StartElementToken(tokenStream.getNamespaceURI(), elementName,
794                     location));
795         }
796 
797         addDefineNamespaceTokens();
798 
799         tokenAccumulator.addAll(attributeTokens);
800 
801         if (id != null)
802             componentIds.put(id, location);
803 
804         processBody(state.insideComponent(isComponent));
805     }
806 
807     private void addDefineNamespaceTokens()
808     {
809         for (int i = 0; i < tokenStream.getNamespaceCount(); i++)
810         {
811             String uri = tokenStream.getNamespaceURI(i);
812 
813             
814             
815 
816             if (NAMESPACE_URI_TO_VERSION.containsKey(uri))
817                 continue;
818 
819             if (uri.equals(TAPESTRY_PARAMETERS_URI))
820                 continue;
821 
822             if (uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
823                 continue;
824 
825             tokenAccumulator.add(new DefineNamespacePrefixToken(uri, tokenStream
826                     .getNamespacePrefix(i), getLocation()));
827         }
828     }
829 
830     private TemplateParserState checkForXMLSpaceAttribute(TemplateParserState state)
831     {
832         for (int i = 0; i < tokenStream.getAttributeCount(); i++)
833         {
834             QName qName = tokenStream.getAttributeName(i);
835 
836             if (isXMLSpaceAttribute(qName))
837             {
838                 boolean compress = !"preserve".equals(tokenStream.getAttributeValue(i));
839 
840                 return state.compressWhitespace(compress);
841             }
842         }
843 
844         return state;
845     }
846 
847     
848 
849 
850     private void endElement(TemplateParserState state)
851     {
852         processTextBuffer(state);
853 
854         tokenAccumulator.add(new EndElementToken(getLocation()));
855     }
856 
857     
858 
859 
860 
861 
862     private void classicParameter(TemplateParserState state)
863     {
864         String parameterName = getSingleParameter("name");
865 
866         if (InternalUtils.isBlank(parameterName))
867             throw new TapestryException("The name attribute of the <parameter> element must be specified.",
868                     getLocation(), null);
869 
870         ensureParameterWithinComponent(state);
871 
872         tokenAccumulator.add(new ParameterToken(parameterName, getLocation()));
873 
874         processBody(state.insideComponent(false));
875     }
876 
877     private void ensureParameterWithinComponent(TemplateParserState state)
878     {
879         if (!state.isInsideComponent())
880             throw new RuntimeException(
881                     "Block parameters are only allowed directly within component elements.");
882     }
883 
884     
885 
886 
887 
888     private void parameterElement(TemplateParserState state)
889     {
890         ensureParameterWithinComponent(state);
891 
892         if (tokenStream.getAttributeCount() > 0)
893             throw new TapestryException("A block parameter element does not allow any additional attributes. The element name defines the parameter name.",
894                     getLocation(), null);
895 
896         tokenAccumulator.add(new ParameterToken(tokenStream.getLocalName(), getLocation()));
897 
898         processBody(state.insideComponent(false));
899     }
900 
901     
902 
903 
904 
905 
906     private void body()
907     {
908         tokenAccumulator.add(new BodyToken(getLocation()));
909 
910         while (active)
911         {
912             switch (tokenStream.next())
913             {
914                 case END_ELEMENT:
915                     return;
916 
917                 default:
918                     throw new IllegalStateException(String.format("Content inside a Tapestry body element is not allowed (at %s). The content has been ignored.", getLocation()));
919             }
920         }
921     }
922 
923     
924 
925 
926 
927 
928 
929     private void container(TemplateParserState state)
930     {
931         while (active)
932         {
933             switch (tokenStream.next())
934             {
935                 case START_ELEMENT:
936                     element(state);
937                     break;
938 
939                 
940                 
941 
942                 case END_ELEMENT:
943 
944                     processTextBuffer(state);
945 
946                     return;
947 
948                 default:
949                     textContent(state);
950             }
951         }
952     }
953 
954     
955 
956 
957 
958     private void block(TemplateParserState state)
959     {
960         String blockId = getSingleParameter("id");
961 
962         validateId(blockId, "Block id '%s' is not valid; block ids must be valid Java identifiers: start with a letter, and consist of letters, numbers and underscores.");
963 
964         tokenAccumulator.add(new BlockToken(blockId, getLocation()));
965 
966         processBody(state.insideComponent(false));
967     }
968 
969     private String getSingleParameter(String attributeName)
970     {
971         String result = null;
972 
973         for (int i = 0; i < tokenStream.getAttributeCount(); i++)
974         {
975             QName qName = tokenStream.getAttributeName(i);
976 
977             if (isXMLSpaceAttribute(qName))
978                 continue;
979 
980             if (qName.getLocalPart().equalsIgnoreCase(attributeName))
981             {
982                 result = tokenStream.getAttributeValue(i);
983                 continue;
984             }
985 
986             
987 
988             throw new TapestryException(String.format("Element <%s> does not support an attribute named '%s'. The only allowed attribute name is '%s'.", tokenStream
989                     .getLocalName(), qName.toString(), attributeName), getLocation(), null);
990         }
991 
992         return result;
993     }
994 
995     private void validateId(String id, String messageKey)
996     {
997         if (id == null)
998             return;
999 
1000         if (ID_PATTERN.matcher(id).matches())
1001             return;
1002 
1003         
1004 
1005         throw new TapestryException(String.format(messageKey, id), getLocation(), null);
1006     }
1007 
1008     private boolean isXMLSpaceAttribute(QName qName)
1009     {
1010         return XML_NAMESPACE_URI.equals(qName.getNamespaceURI())
1011                 && "space".equals(qName.getLocalPart());
1012     }
1013 
1014     
1015 
1016 
1017 
1018 
1019 
1020 
1021     private void textContent(TemplateParserState state)
1022     {
1023         switch (tokenStream.getEventType())
1024         {
1025             case COMMENT:
1026                 comment(state);
1027                 break;
1028 
1029             case CDATA:
1030                 cdata(state);
1031                 break;
1032 
1033             case CHARACTERS:
1034             case SPACE:
1035                 characters();
1036                 break;
1037 
1038             default:
1039                 unexpectedEventType();
1040         }
1041     }
1042 
1043     private void characters()
1044     {
1045         if (textStartLocation == null)
1046             textStartLocation = getLocation();
1047 
1048         textBuffer.append(tokenStream.getText());
1049     }
1050 
1051     private void cdata(TemplateParserState state)
1052     {
1053         processTextBuffer(state);
1054 
1055         tokenAccumulator.add(new CDATAToken(tokenStream.getText(), getLocation()));
1056     }
1057 
1058     private void comment(TemplateParserState state)
1059     {
1060         processTextBuffer(state);
1061 
1062         String comment = tokenStream.getText();
1063 
1064         tokenAccumulator.add(new CommentToken(comment, getLocation()));
1065     }
1066 
1067     
1068 
1069 
1070     private void processTextBuffer(TemplateParserState state)
1071     {
1072         if (textBuffer.length() != 0)
1073             convertTextBufferToTokens(state);
1074 
1075         textStartLocation = null;
1076     }
1077 
1078     private void convertTextBufferToTokens(TemplateParserState state)
1079     {
1080         String text = textBuffer.toString();
1081 
1082         textBuffer.setLength(0);
1083 
1084         if (state.isCompressWhitespace())
1085         {
1086             text = compressWhitespaceInText(text);
1087 
1088             if (InternalUtils.isBlank(text))
1089                 return;
1090         }
1091 
1092         addTokensForText(text);
1093     }
1094 
1095     
1096 
1097 
1098 
1099 
1100 
1101 
1102     private String compressWhitespaceInText(String text)
1103     {
1104         String linebreaksReduced = REDUCE_LINEBREAKS_PATTERN.matcher(text).replaceAll("\n");
1105 
1106         return REDUCE_WHITESPACE_PATTERN.matcher(linebreaksReduced).replaceAll(" ");
1107     }
1108 
1109     
1110 
1111 
1112 
1113 
1114 
1115 
1116 
1117 
1118 
1119     private void addTokensForText(String text)
1120     {
1121         Matcher matcher = EXPANSION_PATTERN.matcher(text);
1122 
1123         int startx = 0;
1124 
1125         
1126         
1127         
1128         
1129         
1130         
1131         
1132         
1133         
1134         while (matcher.find())
1135         {
1136             int matchStart = matcher.start();
1137 
1138             if (matchStart != startx)
1139             {
1140                 String prefix = text.substring(startx, matchStart);
1141                 tokenAccumulator.add(new TextToken(prefix, textStartLocation));
1142             }
1143 
1144             
1145             
1146             
1147             
1148             
1149             
1150             
1151             
1152             
1153             
1154             
1155             
1156             
1157             
1158             
1159             
1160             
1161             
1162             
1163             
1164             String expression = matcher.group(1);
1165             
1166             
1167             
1168             int openBraceCount = 1;
1169             int expressionEnd = expression.length();
1170             boolean inQuote = false;
1171             for (int i = 0; i < expression.length(); i++)
1172             {
1173                 char c = expression.charAt(i);
1174                 
1175                 
1176                 if (c == EXPANSION_STRING_DELIMITTER)
1177                 {
1178                     inQuote = !inQuote;
1179                     continue;
1180                 } else if (inQuote)
1181                 {
1182                     continue;
1183                 } else if (c == CLOSE_BRACE)
1184                 {
1185                     openBraceCount--;
1186                     if (openBraceCount == 0)
1187                     {
1188                         expressionEnd = i;
1189                         break;
1190                     }
1191                 } else if (c == OPEN_BRACE)
1192                 {
1193                     openBraceCount++;
1194                 }
1195             }
1196             if (expressionEnd < expression.length())
1197             {
1198                 
1199                 
1200                 tokenAccumulator.add(new ExpansionToken(expression.substring(0, expressionEnd), textStartLocation));
1201                 
1202                 startx = matcher.start(1) + expressionEnd + 1;
1203             } else
1204             {
1205                 tokenAccumulator.add(new ExpansionToken(expression.trim(), textStartLocation));
1206 
1207                 startx = matcher.end();
1208             }
1209         }
1210 
1211         
1212 
1213         if (startx < text.length())
1214             tokenAccumulator.add(new TextToken(text.substring(startx, text.length()),
1215                     textStartLocation));
1216     }
1217 
1218     static enum Version
1219     {
1220         T_5_0(5, 0), T_5_1(5, 1), T_5_3(5, 3), T_5_4(5, 4);
1221 
1222         private int major;
1223         private int minor;
1224 
1225 
1226         private Version(int major, int minor)
1227         {
1228             this.major = major;
1229             this.minor = minor;
1230         }
1231 
1232         
1233 
1234 
1235 
1236         public boolean sameOrEarlier(Version other)
1237         {
1238             if (other == null)
1239                 return false;
1240 
1241             if (this == other)
1242                 return true;
1243 
1244             return major <= other.major && minor <= other.minor;
1245         }
1246     }
1247 
1248 }